/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
 *
 */

import path from 'path';

import { coreParameter, FileSet, Input, Json5Reader, MetricLogType, OHOS_ARK_COMPILE_SOURCE_MAP_DIR, TaskDetails, TaskInputValue } from '@ohos/hvigor';
import { Obfuscation } from '@ohos/hvigor-arkts-compose';
import * as fs from 'fs-extra';
import { CopyFilterSync } from 'fs-extra';
import { minimatch } from 'minimatch';

import { projectOhosConfigManager } from '../../common/global/project-ohos-config-manager.js';
import { BuildArtifactConst, BuildDirConst } from '../../const/build-directory-const.js';
import { ArkTSVersionConst, CommonConst, CommonNodeConst } from '../../const/common-const.js';
import { BuildOptionWithName } from '../../options/build/build-opt.js';
import { CmakeUtil } from '../../utils/cmake-util.js';
import { ExtraParamsForHar, FileUtil } from '../../utils/file-util.js';
import { ohPackageLoader } from '../../utils/loader/file/oh-package-loader.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import {
  checkHasLocalFileDependency,
  collectLocalFileDependency,
  copyParameterizationLocalProductDependencies
} from '../../utils/npm-utils.js';
import { transformRules } from '../../utils/obfuscation-config-resolver.js';
import { INPUT_KEY } from '../abstract-merge-profile.js';
import { TargetTaskService } from '../service/target-task-service.js';
import { OhosHarTask } from '../task/ohos-har-task.js';

/**
 * packageHar前的前置操作，负责在打包前将需要打包的文件放在build/target/cache/下
 */
export abstract class AbstractProcessHarArtifacts extends OhosHarTask {
  protected abstract logger: OhosLogger;
  public readonly hasNativeOption: boolean;
  public readonly cmakeListDir: string | undefined;
  protected readonly _moduleDir: string;
  protected readonly taskTmpDir: string;
  protected readonly processedLibs: string;
  protected readonly resourceTablePath: string;
  protected readonly harProfile: string;
  protected readonly harModuleJson: string;
  protected readonly projectDir: string;

  protected readonly cppTypes: string;
  protected readonly allInvalidPrefix: string[] = [];
  protected readonly resourceDir: string;
  protected readonly generateDir: string;
  protected readonly headerPath: string[];
  protected readonly needCppTypes: boolean;
  protected readonly ignoreFileNames: string[] = ['.gitignore', '.ohpmignore'];
  protected readonly ohpmIgnoreRules: string[] = [];
  protected readonly obfuscationFiles: string[];
  protected readonly releaseWhiteListTemDir: string[] = ['example'];
  protected readonly packingIgnoreFiles: string[] = ['/node_modules', '/oh_modules', '/.preview', '/build', '/.cxx', '/.test'];

  protected needCopyFileNames: string[] = [];

  declareExecutionEnv(): Map<string, string> {
    return new Map<string, string>().set('cwd', this.taskTmpDir);
  }

  declareInputs(): Map<string, TaskInputValue> {
    const map = new Map<string, TaskInputValue>()
      .set('hasNativeOption', this.hasNativeOption)
      .set('needCppTypes', this.needCppTypes)
      .set('harModuleJson', this.harModuleJson)
      .set('isOhpmProject', this.isOhpmProject)
      .set('artifactType', this._harExtendInfo.getArtifactType())
      .set('isByteCodeHar', this.targetService.isByteCodeHar())
      .set(INPUT_KEY.projectConfigAppOpt, JSON.stringify(projectOhosConfigManager.getOverrides()?.signingConfig));
    if (this.hasNativeOption && this.cmakeListDir !== undefined) {
      map.set('cmakeListDir', this.cmakeListDir);
    }
    if (this.hasNativeOption && this.needCppTypes) {
      map.set('cppTypes', this.cppTypes);
    }
    return map;
  }

  @Input() private get ignoreResourcePattern() {
    return this.targetService.getBuildOption().resOptions?.ignoreResourcePattern;
  }

  declareInputFiles(): FileSet {
    const invalidPrefixes = this.allInvalidPrefix.join('|').replace(/\\/g, '\\\\');
    const regExp = RegExp(`^(?!${invalidPrefixes}).*`);
    const fileSet = new FileSet()
      .addEntry(this.harProfile, { isDirectory: true })
      .addEntry(this._moduleDir, { isDirectory: true, test: regExp })
      .addEntry(this.generateDir, { isDirectory: true });
    if (fs.pathExistsSync(this.processedLibs)) {
      fileSet.addEntry(this.processedLibs, { isDirectory: true });
    }
    if (fs.pathExistsSync(this.resourceTablePath)) {
      fileSet.addEntry(this.resourceTablePath, { isDirectory: false });
    }
    for (const pathItem of this.headerPath) {
      const headerDir = path.isAbsolute(pathItem) ? pathItem : path.resolve(this._moduleDir, pathItem);
      if (fs.pathExistsSync(headerDir)) {
        fileSet.addEntry(headerDir);
      }
    }
    return fileSet;
  }

  declareOutputFiles(): FileSet {
    const fileSet = new FileSet().addEntries([this.taskTmpDir], { isDirectory: true });
    if (
      !coreParameter.properties[OHOS_ARK_COMPILE_SOURCE_MAP_DIR] &&
      this.sdkInfo.hasRollUpPluginInEtsLoader &&
      !this.targetService.getBuildOption()?.debuggable
    ) {
      fileSet.addEntry(path.resolve(this.pathInfo.getDefaultSourceMapPath(), BuildArtifactConst.SOURCEMAPS_MAP), { isDirectory: false });
    }
    return fileSet;
  }

  protected constructor(taskService: TargetTaskService, taskDetails: TaskDetails) {
    super(taskService, taskDetails);
    this._moduleDir = this.moduleModel.getProjectDir();
    this.projectDir = this.projectModel.getProjectDir();

    this.taskTmpDir = this.getTaskTempDir(
      this.targetData,
      this.service.isFaMode()
        ? `${this.targetService.getTargetData().getTargetName()}@LegacyPackageHar`
        : `${this.targetService.getTargetData().getTargetName()}@PackageHar`,
      false,
    );
    this.generateDir = this.pathInfo.getGeneratePmDir();

    const headerPath = this.targetService.getBuildOption().nativeLib?.headerPath;
    if (typeof headerPath === 'string') {
      this.headerPath = [headerPath];
    } else if (Array.isArray(headerPath)) {
      this.headerPath = headerPath;
    } else {
      this.headerPath = [];
    }
    this.resourceDir = this.pathInfo.getIntermediatesRes();
    this.processedLibs = this.pathInfo.getIntermediatesStrippedLibsDir();
    this.resourceTablePath = path.resolve(this.resourceDir, BuildArtifactConst.RESOURCE_TABLE_TXT);
    this.harProfile = this.pathInfo.getIntermediatesProcessProfileDir();
    this.harModuleJson = path.resolve(this.taskTmpDir, 'src', 'main', CommonConst.MODULE_JSON5);
    if (CmakeUtil.nativeTaskCondition(this.moduleModel, this.targetData, this.targetService.getBuildOption().externalNativeOptions)) {
      this.cmakeListDir = CmakeUtil.getCmakeListDir(this.moduleModel, this.targetName, this.targetService.getBuildOption().externalNativeOptions);
    }
    this.obfuscationFiles = this.resolveObfuscationFiles();

    // 只考虑固定目录下的cpp的类型声明文件
    this.cppTypes = path.resolve(this._moduleDir, 'src', 'main', 'cpp', 'types');

    this.hasNativeOption = this._harExtendInfo.hasConfiguredNativeOption();
    this.needCppTypes = fs.pathExistsSync(this.cppTypes);
    this.initModuleToplevelPathFilter();

    this.initNeedCopyFileNames();
  }

  private initModuleToplevelPathFilter() {
    const filter = [
      BuildDirConst.LIBS,
      BuildDirConst.BUILD_ROOT,
      CommonNodeConst.NODE_MODULES,
      CommonConst.OH_MODULES,
      '.cxx',
      '.previewer',
      '.hvigor',
      ...this.ignoreFileNames,
    ];

    // 用户根据命令行自定义的build root
    if (!filter.includes(this.pathInfo.getBuildRoot())) {
      filter.push(this.pathInfo.getBuildRoot());
    }
    filter.forEach((baseName) => {
      this.allInvalidPrefix.push(path.resolve(this._moduleDir, baseName));
    });
  }

  private initNeedCopyFileNames() {
    const file = this.isOhpmProject ? CommonConst.OH_PACKAGE_JSON5 : CommonConst.PACKAGE_JSON;
    const srcPath = ohPackageLoader.getNodeOhPackagePath(path.resolve(this._moduleDir, file));
    if (fs.existsSync(srcPath)) {
      const json = Json5Reader.getJson5Obj(srcPath);
      if (json.main) {
        this.needCopyFileNames.push(json.main);
      }
    }
    this.needCopyFileNames = this.needCopyFileNames.map((filePath) => path.resolve(this._moduleDir, filePath));
  }

  private initFilterRules(): void {
    if (!this.isOhpmProject) {
      return;
    }
    const { include, exclude } = this.targetService.getBuildOption()?.packingOptions?.asset || {};
    // 配置include或者exclude后，.gitignore和.ohpmignore文件不再影响打包逻辑
    if (!include?.length && !exclude?.length) {
      for (const ignoreFile of this.ignoreFileNames) {
        const ignorePath = path.resolve(this._moduleDir, ignoreFile);
        if (!fs.existsSync(ignorePath) || fs.statSync(ignorePath).isDirectory()) {
          continue;
        }

        // refer to the specification of .gitignore
        const rules = fs
          .readFileSync(ignorePath)
          .toString()
          .split(/\r?\n/)
          .filter((line) => !/^#|^$/.test(line.trim()))
          .map((line) => line.trim());
        this.ohpmIgnoreRules.push(...rules);
      }
    } else {
      // 配置include或者exclude后，这些文件夹默认不打包
      this.ohpmIgnoreRules.push(...this.packingIgnoreFiles);
    }
    for (const pathItem of this.headerPath) {
      this.ohpmIgnoreRules.push(pathItem);
    }

    // 忽略LocalTest源码
    const localTestSourceCodeIgnoreRule = '/src/test';
    this.ohpmIgnoreRules.push(localTestSourceCodeIgnoreRule);

    // 忽略ohosTest源码
    const ohosTestSourceCodeIgnoreRule = '/src/ohosTest';
    this.ohpmIgnoreRules.push(ohosTestSourceCodeIgnoreRule);

    // 排除掉其他target的sourceRoots
    this.initSourceRootFilterRules();
  }

  private initSourceRootFilterRules() {
    const targetOpt = this.targetData.getTargetOpt();
    const sourceRoots = targetOpt.source?.sourceRoots;
    if (!sourceRoots?.length) {
      return;
    }
    const targetName = targetOpt.name;
    const targetOptions = this.moduleModel.getTargetOptions();
    const reg = /^\.[/\\]/;
    const sourceRootSet = sourceRoots.reduce((collection, sourceRoot) => {
      collection.add(sourceRoot.replace(reg, ''));
      return collection;
    }, new Set<string>());
    targetOptions.forEach((targetOption) => {
      if (targetOption.name === targetName) {
        return;
      }
      targetOption.source?.sourceRoots?.forEach((sourceRoot) => {
        const rule = sourceRoot.replace(reg, '');
        if (!sourceRootSet.has(rule)) {
          this.ohpmIgnoreRules.push(rule);
        }
      });
    });
  }

  abstract copyCompiledSourceFileToTempDir(): void;

  abstract processPackageJson(): void;

  abstract processSourceMaps(): void;

  abstract copyDeclgenToTempDir(): void;

  protected async doTaskAction(): Promise<void> {
    const subTaskName = 'verify and initialize packaging information';
    const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
    subDurationEvent.start();

    this.preCheckBeforePack();

    /**
     * 仅供ArkTS1.2使用
     * 拷贝声明文件到目标目录
     */
    this.copyLocalDepsFileToTempDir();

    if (this.targetService.getSelfArkTSVersion() === ArkTSVersionConst.ARKTS1_2 && this.targetService.isByteCodeHar()) {
      this.copyDeclgenToTempDir();
    }

    if (this._harExtendInfo.isObfuscatedHar() || this.targetService.isByteCodeHar()) {
      // release 模式
      // 处理ObfuscatedHar或者ByteCodeHar的资源文件，应用过滤规则
      this.copyResourceFileOfReleaseHar();

      this.copyModuleSourceFileToTempDirByWhiteList();

      this.copyCompiledSourceFileToTempDir();

      // 针对特殊文件进行特殊处理，如果有打包，如果没有不报错
      FileUtil.copySpecialFileToTempDir(this._moduleDir, this.taskTmpDir);
    } else {
      // debug 模式
      this.initFilterRules();

      this.copyModuleSourceFileToTempDirByBlackList();
    }
    FileUtil.disposalPackingOptionAsset(this._moduleDir, this.targetService.getBuildOption()?.packingOptions?.asset, this.taskTmpDir);

    await this.syncNativeHeader();

    await this.syncObfuscationFile();

    this.copyBuildIntermediatesOutToTempDir();

    this.copyIntermediatePacToTempDir();

    this.copyOtherGenerateFiles();

    this.processBundleDepRes();

    this.processPackageJson();

    this.processSourceMaps();

    this.processHarRouterMap();

    this.processHarResStartupConfig();

    this.processObfuscatedHarAbility();

    const useNormalizedOHMUrl = this.targetService.getBuildOption()?.strictMode?.useNormalizedOHMUrl;
    const extraParam: ExtraParamsForHar = CmakeUtil.getHarExtraParams(this);
    let libsFileNameSet = new Set<string>();
    const libsDir = this.pathInfo.getIntermediatesProcessLibs();
    if (fs.existsSync(libsDir)) {
      libsFileNameSet = await FileUtil.readDirFileNames(libsDir);
    }
    FileUtil.addParamToOhPack(this.isOhpmProject, this.taskTmpDir, { useNormalizedOHMUrl }, extraParam, this.targetName, libsFileNameSet);
    subDurationEvent.stop();
    subDurationEvent.setLog(subTaskName, MetricLogType.INFO);
  }

  private async syncNativeHeader() {
    const fileMap = new Map<string, string>();
    for (const pathItem of this.headerPath) {
      const headerDir = path.isAbsolute(pathItem) ? pathItem : path.resolve(this._moduleDir, pathItem);
      CmakeUtil.checkNativeHeader(headerDir, this.moduleModel, fileMap);
      await fs.copy(headerDir, path.resolve(this.taskTmpDir, 'include'), { recursive: true });
    }
  }

  private async syncObfuscationFile() {
    const obfuscation: Obfuscation | undefined = this.targetService.getBuildOption().arkOptions?.obfuscation;
    if (obfuscation?.ruleOptions?.enable || (obfuscation?.consumerFiles && obfuscation?.consumerFiles?.length > 0)) {
      const exportRulePath: string = this.pathInfo.getObfuscationExportRulePath();
      if (fs.existsSync(exportRulePath)) {
        await fs.copyFile(exportRulePath, path.resolve(this.taskTmpDir, CommonConst.OBFUSCATION_TXT));
      }
    }
  }

  private resolveObfuscationFiles() {
    const excludes: string[] = [];
    if (!this._harExtendInfo.isObfuscatedHar()) {
      return excludes;
    }
    const buildOptionSet: BuildOptionWithName[] = this.moduleModel.getProfileOpt().buildOptionSet ?? [];
    buildOptionSet.forEach((option) => {
      excludes.push(...transformRules(option.arkOptions?.obfuscation?.ruleOptions?.files, this._moduleDir, this.moduleModel, this.targetName));
      excludes.push(...transformRules(option.arkOptions?.obfuscation?.consumerFiles, this._moduleDir, this.moduleModel, this.targetName));
    });
    return excludes;
  }

  private noNeedCopyCmakeList(src: string) {
    return !(this.hasNativeOption && src === this.cmakeListDir);
  }

  private copyLocalDepsFileToTempDir() {
    collectLocalFileDependency(this.packageManagerPath, this.packageJsonObj).forEach((localDepPath) => {
      const absolutePath = path.resolve(this._moduleDir, localDepPath);
      if (fs.pathExistsSync(absolutePath)) {
        fs.copySync(absolutePath, path.resolve(this.taskTmpDir, path.relative(this._moduleDir, absolutePath)));
      }
    });

    // 只有在 oh-package.json5 中以 @param: 参数化依赖了本地产物(如file:../lib.har)时 localProductDependenciesPath 才不会为空，此时本地产物依赖需要打进 har 包
    if (this.localProductDependenciesPath.length > 0) {
      copyParameterizationLocalProductDependencies({
        localProductDependencies: this.localProductDependenciesPath,
        moduleDir: this._moduleDir,
        tempDir: this.taskTmpDir,
        logger: this.logger,
      });
    }
  }


  /**
   * 获取resource目录，返回的是从模块根目录下的[relative filePaths]数组
   * 首先获取用户配置的resource目录，如果获取不到，则使用默认的./src/main/resource路径
   * @return {string[]}
   */
  private getResourcesDirectories(): string[] {
    const directories = this.targetService.getTargetData().getTargetOpt().resource?.directories;
    if (directories?.length){
      return directories;
    }
    return [path.relative(this.targetService.getTargetData().getPathInfo().getModulePath(), this.targetData.getModuleSourceSetModel().getTargetResPath())];
  }

  /**
   * 检查给定的文件路径是否在资源目录中
   * @return 如果文件路径在资源目录中，返回true，否则返回false
   * @param absoluteFilePath
   */
  private isResourcePath(absoluteFilePath: string){
    return this.getResourcesDirectories().some(directory => absoluteFilePath.includes(directory));
  }

  /**
   * 复制混淆Har或者字节码har文件的资源文件
   * 复制过程中会过滤掉被忽略的资源文件
   */
  private copyResourceFileOfReleaseHar(){
    this.getResourcesDirectories().forEach(directory => {
      const resourcePath = FileUtil.convertToAbsolutePath(directory, this.targetService.getTargetData().getPathInfo().getModulePath());
      if (fs.existsSync(resourcePath)) {
        const tempResourcesDir = FileUtil.convertToAbsolutePath(directory, this.taskTmpDir);
        fs.copySync(resourcePath, tempResourcesDir, {
          filter: (src) => !this.isResourceIgnored(src)
        });
      }
    });
  }

  private copyModuleSourceFileToTempDirByWhiteList() {
    //  打releasehar包时，增加目录或者文件的白名单  在har 根目录下。
    for (const item of this.releaseWhiteListTemDir) {
      const itemPath = path.resolve(this._moduleDir, item);
      if (fs.existsSync(itemPath)) {
        const itemTargetPath = path.resolve(this.taskTmpDir, item);
        fs.copySync(itemPath, itemTargetPath, { overwrite: false });
      }
    }
  }

  private getCopyFilter(rootPath: string): CopyFilterSync {
    return (src) => {
      return (this.noNeedCopyCmakeList(src) && !(this.isResourcePath(src) && this.isResourceIgnored(src)) && !this.needIgnoreSrc(rootPath, src) && !this.excludeObfuscationFiles(src)) || this.needCopyFile(src);
    };
  }

  private needCopyFile(srcPath: string): boolean {
    return this.needCopyFileNames.indexOf(srcPath) !== -1;
  }

  /**
   * 用户配置的glob语法，用来过滤har打包时候的resource目录，包括字节码、混淆、源码场景
   *
   * @param src absolute file path
   * @private
   */
  private isResourceIgnored(src: string){
    const fileName = path.basename(src);
    const ignoreResourcePattern = this.ignoreResourcePattern;
    if (!ignoreResourcePattern){
      return false;
    }
    return ignoreResourcePattern.some((pattern: string) => minimatch(fileName, pattern));
  }

  /**
   * 过滤混淆配置文件
   *
   * @param src absolute file path
   * @private
   */
  private excludeObfuscationFiles(src: string): boolean {
    return !!this.obfuscationFiles.find((file) => path.relative(src, file) === '');
  }

  private needIgnoreSrc(rootPath: string, srcPath: string) {
    const beginLength = rootPath.length;
    const relativePath = srcPath.substring(beginLength + 1);
    const unixPath = relativePath.split(path.sep).join('/');

    // meet any ignore rules
    return this.ohpmIgnoreRules.some((rule) => {
      return minimatch(unixPath, rule) || minimatch(`/${unixPath}`, rule);
    });
  }

  private copyModuleSourceFileToTempDirByBlackList() {
    // 拷贝模块目录下的所有原始文件
    fs.readdirSync(this._moduleDir).forEach((srcPath) => {
      const src = path.resolve(this._moduleDir, srcPath);
      if (this.allInvalidPrefix.includes(src)) {
        return;
      }
      fs.copySync(src, path.resolve(this.taskTmpDir, srcPath), {
        filter: this.getCopyFilter(this._moduleDir),
      });
    });

    // 开源har打包时将build下的buildProfile.ets文件打到har包中
    const buildProfilePath = this.pathInfo.getHarGenerateBuildProfilePath();
    const removeBuildProfilePath = path.resolve(this.taskTmpDir, BuildArtifactConst.BUILD_PROFILE_FILE);
    if (fs.existsSync(buildProfilePath)) {
      fs.copySync(buildProfilePath, removeBuildProfilePath);
    }

    // 删除源码中拷贝过去多余的module.json5
    if (fs.pathExistsSync(this.harModuleJson)) {
      fs.removeSync(this.harModuleJson);
    }
  }

  private preCheckBeforePack() {
    const packagePath = this.packageManagerPath;
    const packageJsonObj = this.packageJsonObj;

    // 检查package.json依赖中有没有依赖本地文件或文件夹
    if (checkHasLocalFileDependency(packagePath, packageJsonObj)) {
      this.logger
        ._buildError(
          `Local dependencies detected during har packing of module ${this.moduleName}. ` +
            'Declaring local dependencies in har module might cause failing during install har package.',
        )
        ._detail('Check package.json/oh-package.json5 of current library module and replace the local dependencies.')
        ._file(packagePath)
        ._printWarn(this.moduleName);
    }

    // 初始化模块最后用于打包的temp文件夹
    fs.emptyDirSync(this.taskTmpDir);
  }

  private copyBuildIntermediatesOutToTempDir(): void {
    // c++编译产物
    if (fs.pathExistsSync(this.processedLibs)) {
      fs.copySync(this.processedLibs, path.resolve(this.taskTmpDir, BuildDirConst.LIBS));
    }

    // 处理ResourceTable.txt
    if (fs.pathExistsSync(this.resourceTablePath)) {
      fs.copySync(this.resourceTablePath, path.resolve(this.taskTmpDir, BuildArtifactConst.RESOURCE_TABLE_TXT));
    }

    // 字节码har的insightIntent配置文件需要将处理后的进行打包，而不是直接将源文件打包
    const processedInsightIntentJsonPath = path.resolve(this.pathInfo.getIntermediatesResProfilePath(), BuildArtifactConst.INSIGHT_INTENT_JSON);
    if (fs.pathExistsSync(processedInsightIntentJsonPath) && fs.pathExistsSync(this.getByteCodeHarInsightIntentDesTempPath())) {
      // 判断目标路径下存在insightIntent配置文件，说明意图框架以配置，则需要将处理后的insightIntent配置文件覆盖原始配置文件
      fs.copySync(processedInsightIntentJsonPath, this.getByteCodeHarInsightIntentDesTempPath());
    }

    // 将合并后的module.json/config.json复制到对应位置
    fs.copySync(this.harProfile, path.resolve(this.taskTmpDir, BuildDirConst.SRC, BuildDirConst.MAIN));
  }

  // 获取字节码har打包临时目录中的insightIntent.json的路径
  private getByteCodeHarInsightIntentDesTempPath() {
    const absoluteInsightIntentPath = path.resolve(this.pathInfo.getModuleSrcMainResourceBaseProfilePath(), BuildArtifactConst.INSIGHT_INTENT_JSON);
    return path.resolve(this.taskTmpDir, path.relative(this._moduleDir, absoluteInsightIntentPath));
  }

  protected copyOtherGenerateFiles() {
    // default do nothing
  }

  protected processHarRouterMap() {
    // default do nothing
  }

  protected processHarResStartupConfig() {
    // default do nothing
  }

  protected processBundleDepRes(): void {
    // default do nothing
  }

  protected processObfuscatedHarAbility(): void {
    // default do nothing
  }

  protected copyIntermediatePacToTempDir(): void {
    // default do nothing
  }
}
